﻿Imports System.Text
Imports System.Web
Imports System.Net
Imports System.Security.Cryptography

''' <summary>
''' Verifies a yubikey otp against a validation service.
''' </summary>
''' <remarks></remarks>
Public Class YubiVerifier
    Private m_ID As Integer
    Private m_Key As String
    Private m_KeyBytes() As Byte
    Private m_Url As String
    Private m_SettingsFile As String

    Public Sub New(ByVal pSettingsFile As String)
        'load the settings from the provided settings file location.
        m_SettingsFile = pSettingsFile
        m_ID = Integer.Parse(CachedSettings.GetInstance(m_SettingsFile).Settings.AppID)
        m_Key = CachedSettings.GetInstance(m_SettingsFile).Settings.AppKey
        'the shared key is expected to be in base64 encoding
        m_KeyBytes = Convert.FromBase64String(m_Key)
        m_Url = CachedSettings.GetInstance(m_SettingsFile).Settings.Url
        'prep the url to accept our query parameters.
        If Not m_Url.EndsWith("?") Then m_Url = m_Url & "?"
    End Sub

    ''' <summary>
    ''' Append a query parameter to the string builder.
    ''' </summary>
    ''' <param name="pSB"></param>
    ''' <param name="pParamName"></param>
    ''' <param name="pParamValue"></param>
    ''' <remarks></remarks>
    Protected Sub AppendParameter(ByVal pSB As StringBuilder, ByVal pParamName As String, ByVal pParamValue As String)
        If pSB.Length > 0 Then
            pSB.Append("&")
        End If
        pSB.Append(pParamName)
        pSB.Append("=")
        'make sure we have escaped characters, etc.
        pSB.Append(HttpUtility.UrlEncode(pParamValue))
    End Sub

    ''' <summary>
    ''' Genereate a digital signature for some data based on the shared app key.
    ''' </summary>
    ''' <param name="pParams"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Sign(ByVal pParams As String) As String
        Dim _paramBytes As Byte() = Encoding.UTF8.GetBytes(pParams)
        Dim _algo As KeyedHashAlgorithm = KeyedHashAlgorithm.Create("HMACSHA1")
        _algo.Key = m_KeyBytes
        Return Convert.ToBase64String(_algo.ComputeHash(_paramBytes))
    End Function

    ''' <summary>
    ''' based on the returned parameters and signature, verify that this came from the trusted source.
    ''' </summary>
    ''' <param name="pDict"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Protected Function Validate(ByVal pDict As SortedList(Of String, String)) As Boolean
        Dim _check As New StringBuilder
        Dim _validate As String = Nothing
        For Each _key As String In pDict.Keys
            If _key = "h" Then
                _validate = pDict.Item(_key)
                Continue For
            End If
            If _check.Length > 0 Then
                _check.Append("&")
            End If
            _check.Append(_key)
            _check.Append("=")
            _check.Append(pDict.Item(_key))
        Next
        If _validate Is Nothing Then
            Return False
        End If
        'check the signature matches
        Dim _checkstr As String = Sign(_check.ToString)
        If Not _checkstr.Equals(_validate) Then
            Return False
        End If

        Return True
    End Function

    ''' <summary>
    ''' Verify that the user is the proper user for the provided OTP and that the OTP is validated by the validation
    ''' server.
    ''' </summary>
    ''' <param name="pUserName"></param>
    ''' <param name="pOTP"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function Verify(ByVal pUserName As String, ByVal pOTP As String) As Boolean
        Dim _settings As Settings = CachedSettings.GetInstance(m_SettingsFile).Settings

        'does the public id for the user exist in the settings?
        If Not _settings.Users.ContainsKey(pUserName.ToLower) Then
            _settings.LogErrorIfEnabled("User:" & pUserName.ToLower & " is not configured for logon.")
            Return False
        End If
        'obtain the expected public key for the user.
        Dim _pkey As String = _settings.Users.Item(pUserName.ToLower)

        'get the actual public key provided.
        If pOTP.Length < 12 Then
            _settings.LogErrorIfEnabled("One time password was incorrect number of characters.")
            Return False
        End If
        Dim _otpPKey As String = pOTP.Substring(0, 12)

        'compare the public key provided with the expected public key for the provided username.
        If Not _otpPKey.Equals(_pkey) Then
            _settings.LogErrorIfEnabled("Public key did not match for user: " & pUserName.ToLower)
            Return False
        End If

        'construct the request.
        Dim _sb As New StringBuilder
        AppendParameter(_sb, "id", m_ID.ToString)
        AppendParameter(_sb, "otp", pOTP)
        Dim _params As String = _sb.ToString

        'generate a digital signature for our request so that the server can validate our request.
        Dim _signature As String = Sign(_params)
        AppendParameter(_sb, "h", _signature)

        _params = _sb.ToString

        Dim _url As String = m_Url & _params

        'make the request of the webservice, and get the response.
        Dim _req As HttpWebRequest = CType(WebRequest.Create(_url), HttpWebRequest)
        Dim _resp As HttpWebResponse = CType(_req.GetResponse, HttpWebResponse)

        'we need to sort the parameters on the returned message as we need them to be in paramname order
        'in order to verify the digital signature of the response.
        Dim _dict As New Generic.SortedList(Of String, String)
        Dim _sr As New IO.StreamReader(_resp.GetResponseStream)

        Dim _line As String = _sr.ReadLine()
        While Not String.IsNullOrEmpty(_line)
            Dim _strs() As String = _line.Split("=".ToCharArray, 2)
            If _strs.Length <> 2 Then
                _settings.LogErrorIfEnabled("Server returned an unanticipated response.")
                Return False
            End If
            _dict.Add(_strs(0), _strs(1))
            _line = _sr.ReadLine
        End While
        If Not _dict.ContainsKey("status") Then
            _settings.LogErrorIfEnabled("Service did not return a status")
            Return False
        End If
        'Server should return that the status is ok if the OTP is valid.
        If _dict.Item("status") <> "OK" Then
            _settings.LogErrorIfEnabled("Service did not return status=OK, actual: " & _dict.Item("status"))
            Return False
        End If

        'if the digital signature of the message is valid, then we can proceed.
        If Validate(_dict) Then
            Return True
        Else
            _settings.LogErrorIfEnabled("Digital signature on response did not validate.")
            Return False
        End If
        Return False
    End Function
End Class
